﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using common;
using i_core;
using lmsft.Media;
using lmsft.Media.Codec.Audio;
using lmsft.RTP;
using lmsft.SDP;
using lmsft.SIP.Stack;
using lmsft.UPnP.NAT;

namespace lmsft_sip {
	public class anwser_sdp_stack : IDisposable {
		private UPnP_NAT_Client sip_upnp = new UPnP_NAT_Client();
		private SDP_MediaDescription offer_media;
		private SDP_MediaDescription answer_media;
		private RTP_Session rtp_session;
		public byte[] receive_data;
		private SIP_Stack_Extension sip_stack;

		public i_log log;

		public AudioCodec local_codec;

		public anwser_sdp_stack() {
			receive_data = new byte[1];
		}

		/// <summary>
		/// Processes media offer.
		/// </summary>
		/// <param name="dialog">SIP dialog.</param>
		/// <param name="transaction">Server transaction</param>
		/// <param name="rtpMultimediaSession">RTP multimedia session.</param>
		/// <param name="offer">Remote-party SDP offer.</param>
		/// <param name="localSDP">Current local SDP.</param>
		/// <exception cref="ArgumentNullException">Is raised <b>dialog</b>,<b>transaction</b>,<b>rtpMultimediaSession</b>,<b>offer</b> or <b>localSDP</b> is null reference.</exception>
		public void build(
			SIP_Stack stack,
			SIP_Dialog dialog,
			SIP_ServerTransaction transaction,
			RTP_MultimediaSession rtp_multi_media_session,
			SDP_Message offer,
			SDP_Message localSDP) {
			if (dialog == null) {
				throw new ArgumentNullException("dialog");
			}
			if (transaction == null) {
				throw new ArgumentNullException("transaction");
			}
			if (rtp_multi_media_session == null) {
				throw new ArgumentNullException("rtpMultimediaSession");
			}
			if (offer == null) {
				throw new ArgumentNullException("offer");
			}
			if (localSDP == null) {
				throw new ArgumentNullException("localSDP");
			}

			try {
				sip_stack = stack as SIP_Stack_Extension;

				#region SDP basic validation

				// Version field must exist.

				if (offer.Version == null) {
					transaction.SendResponse(stack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error + ": Invalid SDP answer: Required 'v'(Protocol Version) field is missing.", transaction.Request));

					return;
				}

				// Origin field must exist.
				if (offer.Origin == null) {
					transaction.SendResponse(stack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error + ": Invalid SDP answer: Required 'o'(Origin) field is missing.", transaction.Request));

					return;
				}

				// Session Name field.

				// Check That global 'c' connection attribute exists or otherwise each enabled media stream must contain one.
				if (offer.Connection == null) {
					for (var i = 0; i < offer.MediaDescriptions.Count; i++) {
						if (offer.MediaDescriptions[i].Connection == null) {
							transaction.SendResponse(stack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error + ": Invalid SDP answer: Global or per media stream no: " + i + " 'c'(Connection) attribute is missing.", transaction.Request));

							return;
						}
					}
				}

				#endregion

				// Re-INVITE media streams count must be >= current SDP streams.
				if (localSDP.MediaDescriptions.Count > offer.MediaDescriptions.Count) {
					transaction.SendResponse(stack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error + ": re-INVITE SDP offer media stream count must be >= current session stream count.", transaction.Request));

					return;
				}

				var audioAccepted = false;
				// Process media streams info.
				for (var i = 0; i < offer.MediaDescriptions.Count; i++) {
					//video media is not supported
					if (offer.MediaDescriptions[i].MediaType == "video")
						continue;

					offer_media = offer.MediaDescriptions[i];
					answer_media = (localSDP.MediaDescriptions.Count > i ? localSDP.MediaDescriptions[i] : null);

					// Disabled stream.
					if (offer_media.Port == 0) {
						// Remote-party offered new disabled stream.
						if (answer_media == null) {
							// Just copy offer media stream data to answer and set port to zero.
							localSDP.MediaDescriptions.Add(offer_media);
							localSDP.MediaDescriptions[i].Port = 0;
						}
						// Existing disabled stream or remote party disabled it.
						else {
							answer_media.Port = 0;

							#region Cleanup active RTP stream and it's resources, if it exists

							// Dispose existing RTP session.
							if (answer_media.Tags.ContainsKey("rtp_session")) {
								((RTP_Session)offer_media.Tags["rtp_session"]).Dispose();

								answer_media.Tags.Remove("rtp_session");
							}

							// Release UPnPports if any.
							if (answer_media.Tags.ContainsKey("upnp_rtp_map")) {
								try {
									sip_upnp.DeletePortMapping((UPnP_NAT_Map)answer_media.Tags["upnp_rtp_map"]);
								} catch {
								}
								answer_media.Tags.Remove("upnp_rtp_map");
							}
							if (answer_media.Tags.ContainsKey("upnp_rtcp_map")) {
								try {
									sip_upnp.DeletePortMapping((UPnP_NAT_Map)answer_media.Tags["upnp_rtcp_map"]);
								} catch {
								}
								answer_media.Tags.Remove("upnp_rtcp_map");
							}

							#endregion
						}
					}
					// Remote-party wants to communicate with this stream.
					else {
						// See if we can support this stream.
						if (!audioAccepted && new rtp_media().CanSupportMedia(offer_media)) {
							// New stream.
							if (answer_media == null) {
								answer_media = new SDP_MediaDescription(SDP_MediaTypes.audio, 0, 2, "RTP/AVP", null);
								localSDP.MediaDescriptions.Add(answer_media);
							}

							#region Build audio codec map with codecs which we support
							log.Debug($"build audio codec(local={sip_stack.rtp_codec}, remote={offer_media.MediaFormats.ToSeriableString()}) map");
							var audioCodecs = new rtp_media().GetOurSupportedAudioCodecs(offer_media);
							if (audioCodecs.Count <= 0)
								throw new Exception($"don't support audio codec({offer_media.MediaFormats.ToSeriableString()})");

							answer_media.MediaFormats.Clear();
							answer_media.Attributes.Clear();
							foreach (var entry in audioCodecs) {
								if (entry.Value.Name != sip_stack.rtp_codec)
									continue;
								local_codec = entry.Value;
								log.Debug("audio codec: {0}", local_codec.Name);
								answer_media.Attributes.Add(new SDP_Attribute("rtpmap", entry.Key + " " + entry.Value.Name + "/" + entry.Value.CompressedAudioFormat.SamplesPerSecond));
								answer_media.MediaFormats.Add(entry.Key.ToString());
							}
							answer_media.Attributes.Add(new SDP_Attribute("ptime", "20"));
							answer_media.Tags["audio_codecs"] = audioCodecs;
							#endregion

							#region Create/modify RTP session
							//create rtp session
							{
								if (!answer_media.Tags.ContainsKey("rtp_session")) {
									rtp_session = create_rtp_sesion(rtp_multi_media_session);
									answer_media.Tags.Add("rtp_session", rtp_session);

									bool AudioOutFlag = true;
									rtp_session.NewReceiveStream += delegate (object s, RTP_ReceiveStreamEventArgs e) {
										if (AudioOutFlag == true) {
											if (answer_media.Tags.ContainsKey("rtp_audio_out")) {
												AudioOut_RTP out_rtp = answer_media.Tags["rtp_audio_out"] as AudioOut_RTP;
												if (out_rtp != null && !out_rtp.IsRunning) {
													out_rtp.Dispose();
												}
												answer_media.Tags.Remove("rtp_audio_out");
											}
											AudioOut_RTP audioOut = new AudioOut_RTP(sip_param.m_pAudioOutDevice, e.Stream, audioCodecs);
											audioOut.Start();
											answer_media.Tags.Add("rtp_audio_out", audioOut);
											AudioOutFlag = false;
										}
									};

									// NAT
									if (!new nat_proc().nat_handle(answer_media, rtp_session)) {
										// NAT handling failed,disable this stream.
										answer_media.Port = 0;
										break;
									}
								}
							}

							var offerStreamMode = new rtp_media().GetRtpStreamMode(offer, offer_media);
							switch (offerStreamMode) {
								case RTP_StreamMode.Inactive:
									answer_media.SetStreamMode("inactive");
									break;
								case RTP_StreamMode.Receive:
									answer_media.SetStreamMode("sendonly");
									break;
								case RTP_StreamMode.Send:
									answer_media.SetStreamMode("recvonly");
									break;
								case RTP_StreamMode.SendReceive:
									answer_media.SetStreamMode("sendrecv");
									break;
							}

							var rtpSession = (RTP_Session)answer_media.Tags["rtp_session"];
							rtpSession.Payload = Convert.ToInt32(answer_media.MediaFormats[0]);
							rtpSession.StreamMode = new rtp_media().GetRtpStreamMode(localSDP, answer_media);
							rtpSession.RemoveTargets();
							if (new rtp_media().GetSdpHost(offer, offer_media) != "0.0.0.0") {
								rtpSession.AddTarget(new rtp_media().GetRtpTarget(offer, offer_media));
							}
							rtpSession.Start();

							#endregion

							#region Create/modify audio-in source

							if (!answer_media.Tags.ContainsKey("rtp_audio_in")) {
								var rtpAudioIn = new AudioIn_RTP(sip_param.m_pAudioInDevice, 20, audioCodecs, rtpSession.CreateSendStream());
								rtpAudioIn.Start();
								answer_media.Tags.Add("rtp_audio_in", rtpAudioIn);
							} else {
								((AudioIn_RTP)answer_media.Tags["rtp_audio_in"]).AudioCodecs = audioCodecs;
							}

							#endregion

							audioAccepted = true;
						}
						// We don't accept this stream, so disable it.
						else {
							// Just copy offer media stream data to answer and set port to zero.

							// Delete exisiting media stream.
							if (answer_media != null) {
								localSDP.MediaDescriptions.RemoveAt(i);
							}
							localSDP.MediaDescriptions.Add(offer_media);
							localSDP.MediaDescriptions[i].Port = 0;
						}
					}
				}

				#region Create and send 2xx response

				var response = stack.CreateResponse(SIP_ResponseCodes.x200_Ok, transaction.Request, transaction.Flow);
				//response.Contact = SIP stack will allocate it as needed;
				response.ContentType = "application/sdp";
				response.Data = localSDP.ToByte();

				transaction.SendResponse(response);

				// Start retransmitting 2xx response, while ACK receives.
				new rtp_media().Handle2xx(dialog, transaction);

				#endregion
			} catch (Exception x) {
				log.Error("build answer sdb error", x);
				transaction.SendResponse(stack.CreateResponse(SIP_ResponseCodes.x500_Server_Internal_Error + ": " + x.Message, transaction.Request));
			}
		}

		/// <summary>
		/// is raise by recieve a new stream
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void rtp_session_receive_stream(object sender, RTP_ReceiveStreamEventArgs e) {
			if (answer_media == null) {
				log.Error("answer media is null, can't save receive stream");
				return;
			}
			if (answer_media.Tags.ContainsKey("rtp_receive_stream")) {
				answer_media.Tags["rtp_receive_stream"] = e.Stream;
			} else
				answer_media.Tags.Add("rtp_receive_stream", e.Stream);

			try {
				log.Debug("recieve a audio stream");
				var rtp_receive_stream = e.Stream as RTP_ReceiveStream;
				rtp_receive_stream.PacketReceived += new EventHandler<RTP_PacketEventArgs>(
					(object stream, RTP_PacketEventArgs p_e) => {
						try {

							var data = p_e.Packet.Data;
							var decode_data = local_codec.Decode(data, 0, data.Length);
							receive_data = receive_data.Merge(decode_data);
						} catch (Exception ex) {
							log.Error("save rtp data error", ex);
						}
					});
			} catch (Exception ex) {
				log.Error("proccess receive stream error", ex);
			}
		}

		/// <summary>
		/// create a new rtp_session
		/// </summary>
		/// <param name="rtp_multi_media_session"></param>
		/// <returns></returns>
		private RTP_Session create_rtp_sesion(RTP_MultimediaSession rtp_multi_media_session) {
			if (rtp_multi_media_session == null) {
				throw new ArgumentNullException("rtp_multi_media_session");
			}


			if (string.IsNullOrEmpty(sip_stack.rtp_ip)) {
				throw new Exception("rtp_ip is available.");
			}
			try {
				RTP_Session rtp_session = null;
				var rtp_port = random_helper.get_random_int(50000, 59999);
				for (var i = 0; i < 100; i += 2) {
					try {
						rtp_session = rtp_multi_media_session.CreateSession(
							new RTP_Address(IPAddress.Parse(sip_stack.rtp_ip), rtp_port, rtp_port + 1),
							new RTP_Clock(1, local_codec.CompressedAudioFormat.SamplesPerSecond)
						);
						break;
					} catch {
						rtp_port += 2;
					}
				}
				log.Debug($"rtp port is : { rtp_port}, rtp control port: {rtp_port + 1}");
				if (rtp_session == null) {
					throw new Exception("rtp port is not available");
				}

				rtp_session.NewReceiveStream += rtp_session_receive_stream;

				return rtp_session;
			} catch (Exception ex) {
				throw new Exception("create rtp_session failed: " + ex.Message);
			}
		}

		#region IDisposable Support
		private bool disposedValue = false; // 要检测冗余调用

		protected virtual void Dispose(bool disposing) {
			if (!disposedValue) {
				if (disposing) {
					// TODO: 释放托管状态(托管对象)。
				}

				// TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
				// TODO: 将大型字段设置为 null。

				disposedValue = true;
			}
		}

		// TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
		// ~anwser_sdp_stack() {
		//   // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
		//   Dispose(false);
		// }

		// 添加此代码以正确实现可处置模式。
		public void Dispose() {
			// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
			Dispose(true);
			// TODO: 如果在以上内容中替代了终结器，则取消注释以下行。
			// GC.SuppressFinalize(this);
		}
		#endregion
	}
}
